/*
 * transform: A jQuery cssHooks adding cross-browser 2d transform capabilities to $.fn.css() and $.fn.animate()
 *
 * limitations:
 * - requires jQuery 1.4.3+
 * - Should you use the *translate* property, then your elements need to be absolutely positionned in a relatively positionned wrapper **or it will fail in IE678**.
 * - transformOrigin is not accessible
 *
 * latest version and complete README available on Github:
 * https://github.com/louisremi/jquery.transform.js
 *
 * Copyright 2011 @louis_remi
 * Licensed under the MIT license.
 *
 * This saved you an hour of work?
 * Send me music http://www.amazon.co.uk/wishlist/HNTU0468LQON
 *
 */
(function ($, window, document, Math, undefined) {

  /*
   * Feature tests and global variables
   */
  var div = document.createElement("div"),
    divStyle = div.style,
    suffix = "Transform",
    testProperties = [
      "O" + suffix,
      "ms" + suffix,
      "Webkit" + suffix,
      "Moz" + suffix
    ],
    i = testProperties.length,
    supportProperty,
    supportMatrixFilter,
    supportFloat32Array = "Float32Array" in window,
    propertyHook,
    propertyGet,
    rMatrix = /Matrix([^)]*)/,
    rAffine = /^\s*matrix\(\s*1\s*,\s*0\s*,\s*0\s*,\s*1\s*(?:,\s*0(?:px)?\s*){2}\)\s*$/,
    _transform = "transform",
    _transformOrigin = "transformOrigin",
    _translate = "translate",
    _rotate = "rotate",
    _scale = "scale",
    _skew = "skew",
    _matrix = "matrix";

  // test different vendor prefixes of these properties
  while (i--) {
    if (testProperties[i] in divStyle) {
      $.support[_transform] = supportProperty = testProperties[i];
      $.support[_transformOrigin] = supportProperty + "Origin";
      continue;
    }
  }
  // IE678 alternative
  if (!supportProperty) {
    $.support.matrixFilter = supportMatrixFilter = divStyle.filter === "";
  }

  // px isn't the default unit of these properties
  $.cssNumber[_transform] = $.cssNumber[_transformOrigin] = true;

  /*
   * fn.css() hooks
   */
  if (supportProperty && supportProperty != _transform) {
    // Modern browsers can use jQuery.cssProps as a basic hook
    $.cssProps[_transform] = supportProperty;
    $.cssProps[_transformOrigin] = supportProperty + "Origin";

    // Firefox needs a complete hook because it stuffs matrix with "px"
    if (supportProperty == "Moz" + suffix) {
      propertyHook = {
        get: function (elem, computed) {
          return (computed ?
            // remove "px" from the computed matrix
            $.css(elem, supportProperty).split("px").join("") :
            elem.style[supportProperty]
          );
        },
        set: function (elem, value) {
          // add "px" to matrices
          elem.style[supportProperty] = /matrix\([^)p]*\)/.test(value) ?
            value.replace(/matrix((?:[^,]*,){4})([^,]*),([^)]*)/, _matrix + "$1$2px,$3px") :
            value;
        }
      };
      /* Fix two jQuery bugs still present in 1.5.1
       * - rupper is incompatible with IE9, see http://jqbug.com/8346
       * - jQuery.css is not really jQuery.cssProps aware, see http://jqbug.com/8402
       */
    } else if (/^1\.[0-5](?:\.|$)/.test($.fn.jquery)) {
      propertyHook = {
        get: function (elem, computed) {
          return (computed ?
            $.css(elem, supportProperty.replace(/^ms/, "Ms")) :
            elem.style[supportProperty]
          );
        }
      };
    }
    /* TODO: leverage hardware acceleration of 3d transform in Webkit only
    else if ( supportProperty == "Webkit" + suffix && support3dTransform ) {
      propertyHook = {
        set: function( elem, value ) {
          elem.style[supportProperty] = 
            value.replace();
        }
      }
    }*/

  } else if (supportMatrixFilter) {
    propertyHook = {
      get: function (elem, computed, asArray) {
        var elemStyle = (computed && elem.currentStyle ? elem.currentStyle : elem.style),
          matrix, data;

        if (elemStyle && rMatrix.test(elemStyle.filter)) {
          matrix = RegExp.$1.split(",");
          matrix = [
            matrix[0].split("=")[1],
            matrix[2].split("=")[1],
            matrix[1].split("=")[1],
            matrix[3].split("=")[1]
          ];
        } else {
          matrix = [1, 0, 0, 1];
        }

        if (!$.cssHooks[_transformOrigin]) {
          matrix[4] = elemStyle ? parseInt(elemStyle.left, 10) || 0 : 0;
          matrix[5] = elemStyle ? parseInt(elemStyle.top, 10) || 0 : 0;

        } else {
          data = $._data(elem, "transformTranslate", undefined);
          matrix[4] = data ? data[0] : 0;
          matrix[5] = data ? data[1] : 0;
        }

        return asArray ? matrix : _matrix + "(" + matrix + ")";
      },
      set: function (elem, value, animate) {
        var elemStyle = elem.style,
          currentStyle,
          Matrix,
          filter,
          centerOrigin;

        if (!animate) {
          elemStyle.zoom = 1;
        }

        value = matrix(value);

        // rotate, scale and skew
        Matrix = [
          "Matrix(" +
          "M11=" + value[0],
          "M12=" + value[2],
          "M21=" + value[1],
          "M22=" + value[3],
          "SizingMethod='auto expand'"
        ].join();
        filter = (currentStyle = elem.currentStyle) && currentStyle.filter || elemStyle.filter || "";

        elemStyle.filter = rMatrix.test(filter) ?
          filter.replace(rMatrix, Matrix) :
          filter + " progid:DXImageTransform.Microsoft." + Matrix + ")";

        if (!$.cssHooks[_transformOrigin]) {

          // center the transform origin, from pbakaus's Transformie http://github.com/pbakaus/transformie
          if ((centerOrigin = $.transform.centerOrigin)) {
            elemStyle[centerOrigin == "margin" ? "marginLeft" : "left"] = -(elem.offsetWidth / 2) + (elem.clientWidth / 2) + "px";
            elemStyle[centerOrigin == "margin" ? "marginTop" : "top"] = -(elem.offsetHeight / 2) + (elem.clientHeight / 2) + "px";
          }

          // translate
          // We assume that the elements are absolute positionned inside a relative positionned wrapper
          elemStyle.left = value[4] + "px";
          elemStyle.top = value[5] + "px";

        } else {
          $.cssHooks[_transformOrigin].set(elem, value);
        }
      }
    };
  }
  // populate jQuery.cssHooks with the appropriate hook if necessary
  if (propertyHook) {
    $.cssHooks[_transform] = propertyHook;
  }
  // we need a unique setter for the animation logic
  propertyGet = propertyHook && propertyHook.get || $.css;

  /*
   * fn.animate() hooks
   */
  $.fx.step.transform = function (fx) {
    var elem = fx.elem,
      start = fx.start,
      end = fx.end,
      pos = fx.pos,
      transform = "",
      precision = 1E5,
      i, startVal, endVal, unit;

    // fx.end and fx.start need to be converted to interpolation lists
    if (!start || typeof start === "string") {

      // the following block can be commented out with jQuery 1.5.1+, see #7912
      if (!start) {
        start = propertyGet(elem, supportProperty);
      }

      // force layout only once per animation
      if (supportMatrixFilter) {
        elem.style.zoom = 1;
      }

      // replace "+=" in relative animations (-= is meaningless with transforms)
      end = end.split("+=").join(start);

      // parse both transform to generate interpolation list of same length
      $.extend(fx, interpolationList(start, end));
      start = fx.start;
      end = fx.end;
    }

    i = start.length;

    // interpolate functions of the list one by one
    while (i--) {
      startVal = start[i];
      endVal = end[i];
      unit = +false;

      switch (startVal[0]) {

        case _translate:
          unit = "px";
        case _scale:
          unit || (unit = "");

          transform = startVal[0] + "(" +
            Math.round((startVal[1][0] + (endVal[1][0] - startVal[1][0]) * pos) * precision) / precision + unit + "," +
            Math.round((startVal[1][1] + (endVal[1][1] - startVal[1][1]) * pos) * precision) / precision + unit + ")" +
            transform;
          break;

        case _skew + "X":
        case _skew + "Y":
        case _rotate:
          transform = startVal[0] + "(" +
            Math.round((startVal[1] + (endVal[1] - startVal[1]) * pos) * precision) / precision + "rad)" +
            transform;
          break;
      }
    }

    fx.origin && (transform = fx.origin + transform);

    propertyHook && propertyHook.set ?
      propertyHook.set(elem, transform, +true) :
      elem.style[supportProperty] = transform;
  };

  /*
   * Utility functions
   */

  // turns a transform string into its "matrix(A,B,C,D,X,Y)" form (as an array, though)
  function matrix (transform) {
    transform = transform.split(")");
    var
      trim = $.trim
      , i = -1
      // last element of the array is an empty string, get rid of it
      , l = transform.length - 1
      , split, prop, val
      , prev = supportFloat32Array ? new Float32Array(6) : []
      , curr = supportFloat32Array ? new Float32Array(6) : []
      , rslt = supportFloat32Array ? new Float32Array(6) : [1, 0, 0, 1, 0, 0]
      ;

    prev[0] = prev[3] = rslt[0] = rslt[3] = 1;
    prev[1] = prev[2] = prev[4] = prev[5] = 0;

    // Loop through the transform properties, parse and multiply them
    while (++i < l) {
      split = transform[i].split("(");
      prop = trim(split[0]);
      val = split[1];
      curr[0] = curr[3] = 1;
      curr[1] = curr[2] = curr[4] = curr[5] = 0;

      switch (prop) {
        case _translate + "X":
          curr[4] = parseInt(val, 10);
          break;

        case _translate + "Y":
          curr[5] = parseInt(val, 10);
          break;

        case _translate:
          val = val.split(",");
          curr[4] = parseInt(val[0], 10);
          curr[5] = parseInt(val[1] || 0, 10);
          break;

        case _rotate:
          val = toRadian(val);
          curr[0] = Math.cos(val);
          curr[1] = Math.sin(val);
          curr[2] = -Math.sin(val);
          curr[3] = Math.cos(val);
          break;

        case _scale + "X":
          curr[0] = +val;
          break;

        case _scale + "Y":
          curr[3] = val;
          break;

        case _scale:
          val = val.split(",");
          curr[0] = val[0];
          curr[3] = val.length > 1 ? val[1] : val[0];
          break;

        case _skew + "X":
          curr[2] = Math.tan(toRadian(val));
          break;

        case _skew + "Y":
          curr[1] = Math.tan(toRadian(val));
          break;

        case _matrix:
          val = val.split(",");
          curr[0] = val[0];
          curr[1] = val[1];
          curr[2] = val[2];
          curr[3] = val[3];
          curr[4] = parseInt(val[4], 10);
          curr[5] = parseInt(val[5], 10);
          break;
      }

      // Matrix product (array in column-major order)
      rslt[0] = prev[0] * curr[0] + prev[2] * curr[1];
      rslt[1] = prev[1] * curr[0] + prev[3] * curr[1];
      rslt[2] = prev[0] * curr[2] + prev[2] * curr[3];
      rslt[3] = prev[1] * curr[2] + prev[3] * curr[3];
      rslt[4] = prev[0] * curr[4] + prev[2] * curr[5] + prev[4];
      rslt[5] = prev[1] * curr[4] + prev[3] * curr[5] + prev[5];

      prev = [rslt[0], rslt[1], rslt[2], rslt[3], rslt[4], rslt[5]];
    }
    return rslt;
  }

  // turns a matrix into its rotate, scale and skew components
  // algorithm from http://hg.mozilla.org/mozilla-central/file/7cb3e9795d04/layout/style/nsStyleAnimation.cpp
  function unmatrix (matrix) {
    var
      scaleX
      , scaleY
      , skew
      , A = matrix[0]
      , B = matrix[1]
      , C = matrix[2]
      , D = matrix[3]
      ;

    // Make sure matrix is not singular
    if (A * D - B * C) {
      // step (3)
      scaleX = Math.sqrt(A * A + B * B);
      A /= scaleX;
      B /= scaleX;
      // step (4)
      skew = A * C + B * D;
      C -= A * skew;
      D -= B * skew;
      // step (5)
      scaleY = Math.sqrt(C * C + D * D);
      C /= scaleY;
      D /= scaleY;
      skew /= scaleY;
      // step (6)
      if (A * D < B * C) {
        A = -A;
        B = -B;
        skew = -skew;
        scaleX = -scaleX;
      }

      // matrix is singular and cannot be interpolated
    } else {
      // In this case the elem shouldn't be rendered, hence scale == 0
      scaleX = scaleY = skew = 0;
    }

    // The recomposition order is very important
    // see http://hg.mozilla.org/mozilla-central/file/7cb3e9795d04/layout/style/nsStyleAnimation.cpp#l971
    return [
      [_translate, [+matrix[4], +matrix[5]]],
      [_rotate, Math.atan2(B, A)],
      [_skew + "X", Math.atan(skew)],
      [_scale, [scaleX, scaleY]]
    ];
  }

  // build the list of transform functions to interpolate
  // use the algorithm described at http://dev.w3.org/csswg/css3-2d-transforms/#animation
  function interpolationList (start, end) {
    var list = {
      start: [],
      end: []
    },
      i = -1, l,
      currStart, currEnd, currType;

    // get rid of affine transform matrix
    (start == "none" || isAffine(start)) && (start = "");
    (end == "none" || isAffine(end)) && (end = "");

    // if end starts with the current computed style, this is a relative animation
    // store computed style as the origin, remove it from start and end
    if (start && end && !end.indexOf("matrix") && toArray(start).join() == toArray(end.split(")")[0]).join()) {
      list.origin = start;
      start = "";
      end = end.slice(end.indexOf(")") + 1);
    }

    if (!start && !end) { return; }

    // start or end are affine, or list of transform functions are identical
    // => functions will be interpolated individually
    if (!start || !end || functionList(start) == functionList(end)) {

      start && (start = start.split(")")) && (l = start.length);
      end && (end = end.split(")")) && (l = end.length);

      while (++i < l - 1) {
        start[i] && (currStart = start[i].split("("));
        end[i] && (currEnd = end[i].split("("));
        currType = $.trim((currStart || currEnd)[0]);

        append(list.start, parseFunction(currType, currStart ? currStart[1] : 0));
        append(list.end, parseFunction(currType, currEnd ? currEnd[1] : 0));
      }

      // otherwise, functions will be composed to a single matrix
    } else {
      list.start = unmatrix(matrix(start));
      list.end = unmatrix(matrix(end))
    }

    return list;
  }

  function parseFunction (type, value) {
    var
      // default value is 1 for scale, 0 otherwise
      defaultValue = +(!type.indexOf(_scale)),
      scaleX,
      // remove X/Y from scaleX/Y & translateX/Y, not from skew
      cat = type.replace(/e[XY]/, "e");

    switch (type) {
      case _translate + "Y":
      case _scale + "Y":

        value = [
          defaultValue,
          value ?
            parseFloat(value) :
            defaultValue
        ];
        break;

      case _translate + "X":
      case _translate:
      case _scale + "X":
        scaleX = 1;
      case _scale:

        value = value ?
          (value = value.split(",")) && [
            parseFloat(value[0]),
            parseFloat(value.length > 1 ? value[1] : type == _scale ? scaleX || value[0] : defaultValue + "")
          ] :
          [defaultValue, defaultValue];
        break;

      case _skew + "X":
      case _skew + "Y":
      case _rotate:
        value = value ? toRadian(value) : 0;
        break;

      case _matrix:
        return unmatrix(value ? toArray(value) : [1, 0, 0, 1, 0, 0]);
        break;
    }

    return [[cat, value]];
  }

  function isAffine (matrix) {
    return rAffine.test(matrix);
  }

  function functionList (transform) {
    return transform.replace(/(?:\([^)]*\))|\s/g, "");
  }

  function append (arr1, arr2, value) {
    while (value = arr2.shift()) {
      arr1.push(value);
    }
  }

  // converts an angle string in any unit to a radian Float
  function toRadian (value) {
    return ~value.indexOf("deg") ?
      parseInt(value, 10) * (Math.PI * 2 / 360) :
      ~value.indexOf("grad") ?
        parseInt(value, 10) * (Math.PI / 200) :
        parseFloat(value);
  }

  // Converts "matrix(A,B,C,D,X,Y)" to [A,B,C,D,X,Y]
  function toArray (matrix) {
    // remove the unit of X and Y for Firefox
    matrix = /([^,]*),([^,]*),([^,]*),([^,]*),([^,p]*)(?:px)?,([^)p]*)(?:px)?/.exec(matrix);
    return [matrix[1], matrix[2], matrix[3], matrix[4], matrix[5], matrix[6]];
  }

  $.transform = {
    centerOrigin: "margin"
  };

})(jQuery, window, document, Math);